iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
2
AI & Data

用Node.js製作後台零負擔的DiscordBot系列 第 11

Day11 - 音樂系統(2)

  • 分享至 

  • xImage
  •  

以下文章已於 2021/09/16 轉移至 微笑之家
對於discord.js更新,或是有其他問題,都歡迎到以下網址查看喔
本站
本主題
本文章


11-1

今天我們把剩下的功能做完

繼續做之前,我們回顧一下音樂播放的其中一小段

11-2

在playMusic2這段,我們將音樂網址與相關設定打入connection後,connection開始播放歌曲,並且返還控制項;
我們宣告一個dispatcher來接收控制項,並且在下一行監聽finish事件

從這一段會注意到他解決了一件事情,那就是我們該怎麼監測歌曲播放的狀態?

dispatcher這個物件在被賦予後,可以直接當成是我們的遙控器
不管是監聽歌曲是不是播完了,還是調整音量&咖歌等,都會需要調用dispatcher

所以如果之後我們要繼續實作咖歌&循環播放等功能,除了需要調用歌曲清單以外,還需要調用到dispatcher

11-3

這邊在音樂系統的最上方,將dispatcher宣告成全域變數
並且記得原本宣告dispatcher的const要拿掉!

觀念說完,那我們繼續:

中斷歌曲

//?skip
function skipMusic() {
    //將歌曲關閉,觸發finish事件
    if (dispatcher !== undefined) dispatcher.end();
}

重播歌曲

//?replay
function replayMusic() {
    if (musicList.length > 0) {
        //把當前曲目再推一個到最前面
        musicList.unshift(musicList[0]);
        //將歌曲關閉,觸發finish事件
        //finish事件將清單第一首歌排出,然後繼續播放下一首
        if (dispatcher !== undefined) dispatcher.end();
    }
}

顯示歌曲清單

//?queue
async function queueShow(channelID) {
    try {
        if (musicList.length > 0) {
            let info;
            let message = '';
            for (i = 0; i < musicList.length; i++) {
                //從連結中獲取歌曲資訊 標題 總長度等
                info = await ytdl.getInfo(musicList[i]);
                //歌曲標題
                title = info.videoDetails.title;
                //串字串
                message = message + `\n${i+1}. ${title}`;
            }
            //把最前面的\n拿掉
            message = message.substring(1, message.length);
            client.channels.fetch(channelID).then(channel => channel.send(message))
        }
    } catch (err) {
        console.log(err, 'queueShowError');
    }
}

顯示當前歌曲

//?np
async function nowPlayMusic(channelID) {
    try {
        if (dispatcher !== undefined && musicList.length > 0) {
            //從連結中獲取歌曲資訊 標題 總長度等
            const info = await ytdl.getInfo(musicList[0]);
            //歌曲標題
            const title = info.videoDetails.title;
            //歌曲全長(s)
            const songLength = info.videoDetails.lengthSeconds;
            //當前播放時間(ms)
            const nowSongLength = Math.floor(dispatcher.streamTime / 1000);
            //串字串
            const message = `${title}\n${streamString(songLength,nowSongLength)}`;
            client.channels.fetch(channelID).then(channel => channel.send(message))
        }
    } catch (err) {
        console.log(err, 'nowPlayMusicError');
    }
}

//▬▬▬▬▬▬▬▬▬?▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
function streamString(songLength, nowSongLength) {
    let mainText = '?';
    const secondText = '▬';
    const whereMain = Math.floor((nowSongLength / songLength) * 100);
    let message = '';
    for (i = 1; i <= 30; i++) {
        if (i * 3.3 + 1 >= whereMain) {
            message = message + mainText;
            mainText = secondText;
        } else {
            message = message + secondText;
        }
    }
    return message;
}

這樣,我們的音樂系統的基本教學就告一段落

11-4

11-5

11-6

其實音樂功能除了本篇教學的基本以外,還有很多花樣可以玩
這個寫法有個很大的問題是,有多個群組同時在使用時,機器人的歌單會掛掉

筆者明天想先說其他主題,音樂系統的教學先告一段落
因為繼續完善下去的話,程式的可讀性會降低,現在的音樂系統筆者認為是最純粹,最好理解的了
同學們不訪想想該怎麼完善這些問題,我們明天見~


上一篇
Day10 - 音樂系統(1)
下一篇
Day12 - Discord的訊息刪除與更新事件(額外)
系列文
用Node.js製作後台零負擔的DiscordBot31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
msamd
iT邦新手 5 級 ‧ 2021-04-24 17:06:03

作者您好~想請教您一下,因為我是看您的文章入門的(說真的寫得很棒),想問一下replay那邊不知道為什麼一直無法執行,輸入後都無法重複撥放都一次即結束,請問該如何修改麻煩您了!!!

看更多先前的回應...收起先前的回應...
微笑 iT邦研究生 5 級 ‧ 2021-04-27 09:11:41 檢舉

你把你的程式碼貼上來看看
還有discord.js版本

微笑 iT邦研究生 5 級 ‧ 2021-04-27 09:54:23 檢舉

謝謝你喜歡我的文章/images/emoticon/emoticon41.gif

msamd iT邦新手 5 級 ‧ 2021-04-27 22:10:27 檢舉

抱歉我看錯了~您寫的是replay不是loop,我是想問有關loopqueue與loop如何書寫~

微笑 iT邦研究生 5 級 ‧ 2021-04-28 09:13:57 檢舉

replay是在下指令後把正在播放的歌曲A,放到歌曲列表的第一個
這時候歌曲列表最前面會有兩個歌曲A,之後主動觸發finish事件
finish時會將歌曲列表最前面的歌曲排出,並且播放下一首歌
因為下一首歌也是歌曲A,所以看起來就是重播了~

微笑 iT邦研究生 5 級 ‧ 2021-04-28 09:16:33 檢舉

回到你的問題,你說希望做出歌曲清單循環
套在程式內的話就是希望在歌曲結束進入finish事件時,將歌曲A放到歌曲清單最後面,這時候列表的最後一首歌會是剛剛播完的歌,如此不斷循環的結果就是歌曲列表循環了
您可以新增一個loop指令,這個指令會對一個全域的boolean做修改
然後此boolean在finish時,會決定要不要將歌曲A放到陣列後面

1
lim890728
iT邦新手 5 級 ‧ 2021-04-28 16:40:57

作者好~最近剛入門DC機器人製作,想請問你在製作音樂機器人時輸入指令機器人卻無回應,找了很久但不曉得哪邊出了問題/images/emoticon/emoticon41.gif

微笑 iT邦研究生 5 級 ‧ 2021-04-29 09:15:13 檢舉

你可以把你的程式貼上來,以及你呼叫失敗的畫面

lim890728 iT邦新手 5 級 ‧ 2021-05-19 22:26:00 檢舉

後來又仔細檢查了有發現問題已解決~

微笑 iT邦研究生 5 級 ‧ 2021-05-20 16:32:20 檢舉

/images/emoticon/emoticon12.gif

0
wsk
iT邦新手 5 級 ‧ 2021-06-27 12:48:24

已刪除

0
wsk
iT邦新手 5 級 ‧ 2021-06-27 17:29:37

https://ithelp.ithome.com.tw/upload/images/20210628/20138917pqVTpgHVe7.png作者好 想請問一下 在寫機器人的過程中 指令無法執行 一直顯示無法讀取未定義的屬性'長度' 這個該如何解決 這個問題是在撰寫音樂系統以後才遇到的 前面可以正常運行!

微笑 iT邦研究生 5 級 ‧ 2021-06-28 09:04:09 檢舉

別用截圖的,程式碼用三個`包起來
傳上來借我看一下

0
wsk
iT邦新手 5 級 ‧ 2021-06-28 12:45:07
//#region 全域變數
const Discord = require('discord.js');
const client = new Discord.Client();
const ytdl = require('ytdl-core');
const auth = require('./JSONHome/auth.json');
const prefix = require('./JSONHome/prefix.json');
//#endregion
//#region login
client.login(auth.key);

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`);
});
//#endregion
//#region 靖騰
client.on('message', msg => {

//#endregion
//#region 判斷分類   
//前置判斷
    try {
        if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊)
        if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在
        if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送
    } catch (err) {
        return;
    }
//#endregion     
//#region  message

     //字串分析
     try {
        let tempPrefix = '-1';
        const prefixED = Object.keys(prefix); //前綴符號定義
        prefixED.forEach(element => {
            if (msg.content.substring(0, prefix[element].Value.length) === prefix[element].Value) {
                tempPrefix = element;
            }
        });

            //功能實作

            switch (tempPrefix) {
                case '0': //文字回應功能
                    BasicFunction(msg, tempPrefix);
                    break;
                case '1': //音樂指令 
                    MusicFunction(msg);
                    break;
            }       
            switch (cmd[0]) {
                case 'myAvatar':
                    const avatar = GetMyAvatar(msg);
                    if (avatar.files) msg.channel.send(`${msg.author}`, avatar);
        
                break;
            }
            
        } catch (err) {
            console.log('OnMessageError', err);   
        }
});
//#endregion
//#region  music
let musiclist = new Array();

function music(msg) {

    const content = msg.content.substring(prefix[1].value.length);
    const splitText  = ' ';
    const contents = content.split(splitText);

    switch(contents[0]){
        case 'play':
            playmusic(msg.contents);
            break;
        case 'replay':
            break;
        case 'np':
            break;
        case 'queue':
            break;
        case 'skip':
            break;
        case 'disconnect':
            disconnectMusic(msg.guild.id,msg.channel.id);
            break;                
    }
    
}


//?play
async function playmusic(msg,contents) {

    const ur1ED = contents[1];
    try{

        if(ur1ED.substring(0,4) !=='http') return msg.reply('the link is not working.1');
        const validate = await ytdl.validateURL(urlED);
        if(!validate) return msg.reply('the link is not wording.2');




        const info = await ytdl.getInfo(ur1ED);

        if(info.videoDetails) {

            if(msg.member.voice.channel){
                if(!client.voice.connections.get(msg.guild.id)){

                    musiclist.push(ur1ED);

                    msg.member.voice.channel.join()
                       .then(connection =>  {
                           msg.reply('我來了~');
                           const guildID = msg.guild.id;
                           const channelID = msg.channel.id;
                           playmusic2(connection,guildID.channelID);
                       })
                       .catch(err =>{
                            msg.reply('bot進入語音頻道時發生錯誤,請在試一次');
                            console.log(err,'playMusicError2');
                       })
                }else{

                    musiclist.push(ur1ED);
                    msg.reply('已經歌曲加入歌單!');
                }
            }else return msg.reply('你要不要先進頻道');
        }else return msg.reply('the link is not working');
    }catch(err){
    console.log(err,'playmusicerror');
    }
}

//?play 遞迴函式
async function playMusic2(connection, guildID, channelID) {
    try {
        //播放前歌曲清單不能沒有網址
        if (musicList.length > 0) {
            //設定音樂相關參數
            const streamOptions = {
                seek: 0,
                volume: 0.5,
                Bitrate: 192000,
                Passes: 1,
                highWaterMark: 1
            };
            //讀取清單第一位網址
            const stream = await ytdl(musicList[0], {
                filter: 'audioonly',
                quality: 'highestaudio',
                highWaterMark: 26214400 //25ms
            })

            //播放歌曲,並且存入dispatcher
            const dispatcher = connection.play(stream, streamOptions);
            //監聽歌曲播放結束事件
            dispatcher.on("finish", finish => {
                //將清單中第一首歌清除
                if (musicList.length > 0) musicList.shift();
                //播放歌曲
                playMusic2(connection, guildID, channelID);
            })
        } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道
    } catch (err) {
        console.log(err, 'playMusic2Error');
    }
}
//?disconnect


function DisconnectMusic(guildID,channelID) {
    try{
        if(client.voice.connections.get(guildID)){
            musiclist = new Array();
            client.voice.connections.get(guildID).disconnect();

            client.channels.fetch(channelID).then(chamnel => channel.send('晚安~'));

        }else client.voice.fetch(channelID).then(channel => chamnel.send('我還沒進來啦'))

    }catch (err) {
        console.log(err, 'disconnectMusicError');
    }
    
}

//?skip
function skipMusic() {
    //將歌曲關閉,觸發finish事件
    if (dispatcher !== undefined) dispatcher.end();
}
//?replay

function replayMusic() {
    if(musiclist.length> 0) {
        musiclist.unshift(musiclist[0]);
        if(dispatcher !== undefined) dispatcher.end();
    }
    
}
//?queue
async function queueShow(channelID) {
    try{
        if(musiclist.length>0){
            let info;
            let message = '';
            for(i = 0;i<musiclist.length;i++ ) {
                info = await ytdl.getInfo(musiclist[i]);

            title = info.videoDetails.title;

            message =  message + `\n$(i+1). ${title}`;
            }

            message = message.substring(1,message.length);
            client.channels.fetch(channelID).then(channel => channel.send(message))
        }
    }catch(err){
        console.log(err,'queueShowError');

    }   
}

//?np
async function nowPlayMusic(channelID) {
    try{
        if(dispatcher !== undefined && musiclist.length>0) {
            const info = await ytdl.getInfo(musiclist[0]);
            const title  = info.videoDetails.title;

            const songLength = info.videoDetails.lengthSeconds;
            const nowSongLength = Math.floor(dispatcher.streamTime / 1000);
            const message = `${title}\n${streamString(songLength,nowSongLength)}`;
            client.channels.fetch(channelID).then(channel =>channel.send(message))
        }
    }catch(err){
        console.log(err,'nowPlayMusicError');
    }
    
}

//---------?--------------
function streamstring(songLength,nowSongLength) {
    let maintext  = '?';
    const secondtext = '=';
    const wheremain = math.floor((nowSongLength/songLength) *10);
    let message = '';
    for (i =1;i<=30;i++){
        if(i*3.3 +1 >=wheremain) {
            message = message +maintext;
            maintext = secondtext;
        }else{
            message = message +secondtext;
        }
    } 
    return message;
}
//#endregion
//#region  avatar(獲取頭像)
function GetMyAvatar(msg) {
    try {
        return {
            files: [{
                attachment: msg.author.displayAvatarURL('png', true),
                name: 'avatar.jpg'
            }]
        };
    } catch (err) {
        console.log('GetMyAvatar,Error');
    }
}
//#endregion
//#region 隨機
function random(max,min) {
    var rnd = math .floor(math.random()*max)+min;
    return rnd;
}
//#endregion
微笑 iT邦研究生 5 級 ‧ 2021-06-30 15:08:30 檢舉

你看上一張error內容
at node.js 57:64
at node.js 56:18
這表示他幫你查出有問題的程式碼在第56跟57行

我拿你這段程式碼去看,第57行只有一個大括弧
想來這不是你當時的程式碼,或是有做變更

上一次截圖不行是因為你56行的程式碼太長了沒截進去
現在則是因為行數不對,我沒辦法幫你看問題出在哪

建議您run出問題後,將有問題的程式碼再用這個方式丟上來一次,謝謝~

wsk iT邦新手 5 級 ‧ 2021-06-30 15:21:16 檢舉

好的

0
wsk
iT邦新手 5 級 ‧ 2021-06-30 15:22:00

https://ithelp.ithome.com.tw/upload/images/20210630/20138917O9ylphGkzp.png

//#region 全域變數
const Discord = require('discord.js');
const client = new Discord.Client();
const ytdl = require('ytdl-core');
const auth = require('./JSONHome/auth.json');
const prefix = require('./JSONHome/prefix.json');
//#endregion
//#region login
client.login(auth.key);

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`);
});
//#endregion
//#region 靖騰
client.on('message', msg => {

//#endregion
//#region 判斷分類   
//前置判斷
    try {
        if (!msg.guild || !msg.member) return; //訊息內不存在guild元素 = 非群組消息(私聊)
        if (!msg.member.user) return; //幫bot值多拉一層,判斷上層物件是否存在
        if (msg.member.user.bot) return; //訊息內bot值為正 = 此消息為bot發送
    } catch (err) {
        return;
    }
//#endregion     
//#region  message

     //字串分析
     try {
        let tempPrefix = '-1';
        const prefixED = Object.keys(prefix); //前綴符號定義
        prefixED.forEach(element => {
            if (msg.content.substring(0, prefix[element].Value.length) === prefix[element].Value) {
                tempPrefix = element;
            }
        });

            //功能實作

            switch (tempPrefix) {
                case '0': //文字回應功能
                    BasicFunction(msg, tempPrefix);
                    break;
                case '1': //音樂指令 
                    MusicFunction(msg);
                    break;
            }       
            switch (cmd[0]) {
                case 'myAvatar':
                    const avatar = GetMyAvatar(msg);
                    if (avatar.files) msg.channel.send(`${msg.author}`, avatar);
        
                break;
            }
            
        } catch (err) {
            console.log('OnMessageError', err);   
        }
});
//#endregion
//#region  music
let musiclist = new Array();

function music(msg) {

    const content = msg.content.substring(prefix[1].value.length);
    const splitText  = ' ';
    const contents = content.split(splitText);

    switch(contents[0]){
        case 'play':
            playmusic(msg.contents);
            break;
        case 'replay':
            break;
        case 'np':
            break;
        case 'queue':
            break;
        case 'skip':
            break;
        case 'disconnect':
            disconnectMusic(msg.guild.id,msg.channel.id);
            break;                
    }
    
}


//?play
async function playmusic(msg,contents) {

    const ur1ED = contents[1];
    try{

        if(ur1ED.substring(0,4) !=='http') return msg.reply('the link is not working.1');
        const validate = await ytdl.validateURL(urlED);
        if(!validate) return msg.reply('the link is not wording.2');




        const info = await ytdl.getInfo(ur1ED);

        if(info.videoDetails) {

            if(msg.member.voice.channel){
                if(!client.voice.connections.get(msg.guild.id)){

                    musiclist.push(ur1ED);

                    msg.member.voice.channel.join()
                       .then(connection =>  {
                           msg.reply('我來了~');
                           const guildID = msg.guild.id;
                           const channelID = msg.channel.id;
                           playmusic2(connection,guildID.channelID);
                       })
                       .catch(err =>{
                            msg.reply('bot進入語音頻道時發生錯誤,請在試一次');
                            console.log(err,'playMusicError2');
                       })
                }else{

                    musiclist.push(ur1ED);
                    msg.reply('已經歌曲加入歌單!');
                }
            }else return msg.reply('你要不要先進頻道');
        }else return msg.reply('the link is not working');
    }catch(err){
    console.log(err,'playmusicerror');
    }
}

//?play 遞迴函式
async function playMusic2(connection, guildID, channelID) {
    try {
        //播放前歌曲清單不能沒有網址
        if (musicList.length > 0) {
            //設定音樂相關參數
            const streamOptions = {
                seek: 0,
                volume: 0.5,
                Bitrate: 192000,
                Passes: 1,
                highWaterMark: 1
            };
            //讀取清單第一位網址
            const stream = await ytdl(musicList[0], {
                filter: 'audioonly',
                quality: 'highestaudio',
                highWaterMark: 26214400 //25ms
            })

            //播放歌曲,並且存入dispatcher
            const dispatcher = connection.play(stream, streamOptions);
            //監聽歌曲播放結束事件
            dispatcher.on("finish", finish => {
                //將清單中第一首歌清除
                if (musicList.length > 0) musicList.shift();
                //播放歌曲
                playMusic2(connection, guildID, channelID);
            })
        } else disconnectMusic(guildID, channelID); //清空歌單並且退出語音頻道
    } catch (err) {
        console.log(err, 'playMusic2Error');
    }
}
//?disconnect


function DisconnectMusic(guildID,channelID) {
    try{
        if(client.voice.connections.get(guildID)){
            musiclist = new Array();
            client.voice.connections.get(guildID).disconnect();

            client.channels.fetch(channelID).then(chamnel => channel.send('晚安~'));

        }else client.voice.fetch(channelID).then(channel => chamnel.send('我還沒進來啦'))

    }catch (err) {
        console.log(err, 'disconnectMusicError');
    }
    
}

//?skip
function skipMusic() {
    //將歌曲關閉,觸發finish事件
    if (dispatcher !== undefined) dispatcher.end();
}
//?replay

function replayMusic() {
    if(musiclist.length> 0) {
        musiclist.unshift(musiclist[0]);
        if(dispatcher !== undefined) dispatcher.end();
    }
    
}
//?queue
async function queueShow(channelID) {
    try{
        if(musiclist.length>0){
            let info;
            let message = '';
            for(i = 0;i<musiclist.length;i++ ) {
                info = await ytdl.getInfo(musiclist[i]);

            title = info.videoDetails.title;

            message =  message + `\n$(i+1). ${title}`;
            }

            message = message.substring(1,message.length);
            client.channels.fetch(channelID).then(channel => channel.send(message))
        }
    }catch(err){
        console.log(err,'queueShowError');

    }   
}

//?np
async function nowPlayMusic(channelID) {
    try{
        if(dispatcher !== undefined && musiclist.length>0) {
            const info = await ytdl.getInfo(musiclist[0]);
            const title  = info.videoDetails.title;

            const songLength = info.videoDetails.lengthSeconds;
            const nowSongLength = Math.floor(dispatcher.streamTime / 1000);
            const message = `${title}\n${streamString(songLength,nowSongLength)}`;
            client.channels.fetch(channelID).then(channel =>channel.send(message))
        }
    }catch(err){
        console.log(err,'nowPlayMusicError');
    }
    
}

//---------?--------------
function streamstring(songLength,nowSongLength) {
    let maintext  = '?';
    const secondtext = '=';
    const wheremain = math.floor((nowSongLength/songLength) *10);
    let message = '';
    for (i =1;i<=30;i++){
        if(i*3.3 +1 >=wheremain) {
            message = message +maintext;
            maintext = secondtext;
        }else{
            message = message +secondtext;
        }
    } 
    return message;
}
//#endregion
//#region  avatar(獲取頭像)
function GetMyAvatar(msg) {
    try {
        return {
            files: [{
                attachment: msg.author.displayAvatarURL('png', true),
                name: 'avatar.jpg'
            }]
        };
    } catch (err) {
        console.log('GetMyAvatar,Error');
    }
}
//#endregion
//#region 隨機
function random(max,min) {
    var rnd = math .floor(math.random()*max)+min;
    return rnd;
}
//#endregion
看更多先前的回應...收起先前的回應...
wsk iT邦新手 5 級 ‧ 2021-06-30 15:25:51 檢舉

之前的程式碼會跟跑出來的行數會不一樣是應該是因為跑錯資料夾了 不好意思沒注意到

微笑 iT邦研究生 5 級 ‧ 2021-06-30 16:54:45 檢舉

你要看一下你的prefix.json
可能是.VALUE出來的內容非字串

wsk iT邦新手 5 級 ‧ 2021-06-30 19:41:23 檢舉

有了 看起來是prefix那裏的大小寫問題 謝謝

微笑 iT邦研究生 5 級 ‧ 2021-07-01 10:43:58 檢舉

加油喔/images/emoticon/emoticon37.gif

我要留言

立即登入留言